package com.googlecode.nls4j;

import java.util.Collections;
import java.util.EnumSet;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;

/**
 * An implementation of {@linkplain NlsBundle} which supports only
 * {@linkplain Enum}-based keys. The resources are stored in a read-only array
 * of the same length as the size of the enum. The index of a resource in the
 * array corresponds to the {@linkplain Enum#ordinal() key's ordinal}. The
 * implementation of the {@linkplain #get(Enum)} method is very simple and fast:
 * 
 * <pre>
 * public V get(final K key) {
 *     return values[key.ordinal()];
 * }
 * </pre>
 * 
 * @author Andrey Pavlenko
 */
public class EnumBundle<K extends Enum<K> & NlsKey<V>, V> implements
        NlsBundle<K, V> {
    private final Locale locale;
    private final Class<K> keyType;
    private final Set<K> keySet;
    private final Set<K> unknownKeys;
    private final Object[] values;

    /**
     * Create new EnumBundle with the specified key type, locale and fill the
     * bundle with the resources located in the specified map. If a key is not
     * listed in the map, the {@linkplain NlsKey#getDefaultValue(Locale) default
     * value} will be used.
     */
    public EnumBundle(final Locale locale, final Class<K> keyType,
            final Map<K, V> values) throws NullPointerException {
        if (locale == null) {
            throw new NullPointerException("Locale must not be null");
        }

        final Set<K> keySet = EnumSet.allOf(keyType);

        if (keySet.isEmpty()) {
            this.values = new Object[0];
            this.keySet = Collections.emptySet();
            this.unknownKeys = Collections.emptySet();
        } else {
            final Object[] v = new Object[keySet.size()];
            final Set<K> unknownKeys = EnumSet.noneOf(keyType);
            int i = 0;

            for (final K k : keySet) {
                if (values.containsKey(k)) {
                    v[i] = values.get(k);
                } else {
                    unknownKeys.add(k);
                    v[i] = k.getDefaultValue(locale);
                }
                i++;
            }

            this.values = v;
            this.keySet = Collections.unmodifiableSet(keySet);
            this.unknownKeys = unknownKeys.isEmpty() ? Collections
                    .<K> emptySet() : unknownKeys;
        }

        this.locale = locale;
        this.keyType = keyType;
    }

    EnumBundle(final Locale locale, final Class<K> keyType,
            final Set<K> keySet, final Set<K> unknownKeys, final Object[] values)
            throws IllegalArgumentException, NullPointerException {
        if (values.length != keySet.size()) {
            throw new IllegalArgumentException(
                    "The length of the values array must be " + keySet.size());
        }
        if (locale == null) {
            throw new NullPointerException("Locale must not be null");
        }

        this.locale = locale;
        this.keyType = keyType;
        this.values = values;
        this.unknownKeys = unknownKeys.isEmpty() ? Collections.<K> emptySet()
                : unknownKeys;
        this.keySet = keySet.isEmpty() ? Collections.<K> emptySet()
                : Collections.unmodifiableSet(keySet);
    }

    /**
     * Create new EnumBundle and fill it with the resources located in the
     * specified {@code resourceBundle}. The identifier of the resources in the
     * {@code resourceBundle} is {@linkplain NlsKey#getName()}. If a key is not
     * found in this resource bundle, the
     * {@linkplain NlsKey#getDefaultValue(Locale) default value} will be used.
     */
    public static <K extends Enum<K> & NlsKey<V>, V> EnumBundle<K, V> fromResourceBundle(
            final Class<K> keyType, final ResourceBundle resourceBundle) {
        final Set<K> keySet = EnumSet.allOf(keyType);
        final Set<K> unknownKeys = EnumSet.noneOf(keyType);
        final Object[] values = new Object[keySet.size()];
        final Locale locale = resourceBundle.getLocale();
        int i = 0;

        for (final K k : keySet) {
            final String name = k.getName();

            if (resourceBundle.containsKey(name)) {
                values[i++] = resourceBundle.getObject(name);
            } else {
                unknownKeys.add(k);
                values[i++] = k.getDefaultValue(locale);
            }
        }

        return new EnumBundle<K, V>(
                locale, keyType, keySet, unknownKeys, values);
    }

    /**
     * Create new EnumBundle with the specified key type, locale and fill it
     * with {@linkplain NlsKey#getDefaultValue(Locale) default values}.
     */
    public static <K extends Enum<K> & NlsKey<V>, V> EnumBundle<K, V> defaultValuesBundle(
            final Class<K> keyType, final Locale locale) {
        final Set<K> keySet = EnumSet.allOf(keyType);
        final Object[] values = new Object[keySet.size()];
        int i = 0;

        for (final K k : keySet) {
            values[i++] = k.getDefaultValue(locale);
        }

        return new EnumBundle<K, V>(
                locale, keyType, keySet, EnumSet.noneOf(keyType), values);
    }

    @SuppressWarnings("unchecked")
    @Override
    public V get(final K key) {
        return (V) values[key.ordinal()];
    }

    @Override
    public boolean contains(final K key) {
        return keySet.contains(key) && !unknownKeys.contains(key);
    }

    @Override
    public Locale getLocale() {
        return locale;
    }

    /**
     * Returns the type of the NlsKeys identifying resources in this bundle.
     */
    public Class<K> getKeyType() {
        return keyType;
    }

    /**
     * Returns a read-only set of NlsKeys identifying resources in this bundle.
     */
    public Set<K> getKeySet() {
        return keySet;
    }
}
